﻿/*
 * LINQ to LDAP
 * http://linqtoldap.codeplex.com/
 * 
 * Copyright Alan Hatter (C) 2010-2012
 * alan.hatter@gmail.com - http://hattercoding.blogspot.com/
 * 
 * This project is subject to licensing restrictions. Visit http://linqtoldap.codeplex.com/license for more information.
 */

using System;
using System.Collections.Generic;
using System.DirectoryServices.Protocols;
using System.Linq;
using System.Net;
using System.Timers;

namespace LinqToLdap
{
    internal class PooledLdapConnectionFactory : ConnectionFactoryBase, IPooledConnectionFactoryConfiguration, ILdapConnectionFactory, IDisposable
    {
        private bool _disposed;
        private object _connectionLockObject = new object();
        private Dictionary<LdapConnection, DateTime> _availableConnections = new Dictionary<LdapConnection, DateTime>();
        private List<LdapConnection> _inUseConnections = new List<LdapConnection>();
        private int _maxPoolSize = 50;
        private int _minPoolSize;
        private double _connectionIdleTime = 1;
        private Timer _timer;
        private bool _isFirstRequest = true;

        public PooledLdapConnectionFactory(string serverName) : base(serverName)
        {
            _timer = new Timer();
            _timer.Elapsed += TimerElapsed;
            _timer.Interval = 90000;
        }

        ~PooledLdapConnectionFactory()
        {
            InternalDispose();
        }

        public void Dispose()
        {
            _disposed = true;
            InternalDispose();
            GC.SuppressFinalize(this);
        }

        private void InternalDispose()
        {
            lock (_connectionLockObject)
            {
                try
                {
                    var availableConnections = _availableConnections;
                    var inUseConnections = _inUseConnections;
                    var timer = _timer;

                    if (timer != null)
                    {
                        timer.Stop();
                        timer.Elapsed -= TimerElapsed;
                        timer.Dispose();
                        timer = null;
                        _timer = null;
                    }

                    if (availableConnections != null)
                    {
                        foreach (var connection in availableConnections)
                        {
                            try
                            {
                                connection.Key.Dispose();
                            }
                            // ReSharper disable EmptyGeneralCatchClause
                            catch { }
                            // ReSharper restore EmptyGeneralCatchClause
                        }

                        availableConnections.Clear();
                        availableConnections = null;
                        _availableConnections = null;
                    }

                    if (inUseConnections != null)
                    {
                        foreach (var connection in inUseConnections)
                        {
                            try
                            {
                                connection.Dispose();
                            }
                            // ReSharper disable EmptyGeneralCatchClause
                            catch { }
                            // ReSharper restore EmptyGeneralCatchClause
                        }

                        inUseConnections.Clear();
                        inUseConnections = null;
                        _inUseConnections = null;
                    }
                }
                catch (Exception ex)
                {
                    if (Logger != null) Logger.Error(ex);
                    throw;
                }
            }

            _connectionLockObject = null;
            Credentials = null;
        }

        public IPooledConnectionFactoryConfiguration ProtocolVersion(int version)
        {
            LdapProtocolVersion = version;
            return this;
        }

        public IPooledConnectionFactoryConfiguration UsePort(int port)
        {
            UsesSsl = false;
            Port = port;
            return this;
        }

        public IPooledConnectionFactoryConfiguration UseSsl(int port = SslPort)
        {
            UsesSsl = true;
            return this;
        }

        public IPooledConnectionFactoryConfiguration ConnectionTimeoutIn(double seconds)
        {
            if (seconds <= 0) throw new ArgumentException("seconds must be greater than 0");
            Timeout = TimeSpan.FromSeconds(seconds);

            return this;
        }

        public IPooledConnectionFactoryConfiguration ServerNameIsFullyQualified()
        {
            FullyQualifiedDnsHostName = true;
            return this;
        }

        public IPooledConnectionFactoryConfiguration UseUdp()
        {
            IsConnectionless = true;
            return this;
        }

        public IPooledConnectionFactoryConfiguration AuthenticateBy(AuthType authType)
        {
            AuthType = authType;
            return this;
        }

        public IPooledConnectionFactoryConfiguration AuthenticateAs(NetworkCredential credentials)
        {
            Credentials = credentials;
            return this;
        }

        public void ReleaseConnection(LdapConnection connection)
        {
            if (_disposed) throw new ObjectDisposedException(GetType().FullName);
            lock (_connectionLockObject)
            {
                if (_inUseConnections.Remove(connection))
                {
                    _availableConnections.Add(connection, DateTime.Now);
                    if (Logger != null && Logger.TraceEnabled) Logger.Trace("Connection Marked As Available");
                }
                else
                {
                    connection.Dispose();
                    if (Logger != null && Logger.TraceEnabled) Logger.Trace("Connection Disposed");
                }
            }
        }

        public LdapConnection GetConnection()
        {
            if (_disposed) throw new ObjectDisposedException(GetType().FullName);
            LdapConnection connection;
            lock (_connectionLockObject)
            {
                try
                {
                    if (_isFirstRequest)
                    {
                        if (Logger != null && Logger.TraceEnabled) Logger.Trace("Initializing Connection Pool.");
                        for (int i = 0; i < _minPoolSize; i++)
                        {
                            _availableConnections.Add(BuildConnection(), DateTime.Now);
                        }

                        _isFirstRequest = false;
                        _timer.Start();
                        if (Logger != null && Logger.TraceEnabled) Logger.Trace("Scavenge Timer Started.");
                    }

                    var pair = _availableConnections.FirstOrDefault();

                    if (Equals(pair, default(KeyValuePair<LdapConnection, DateTime>)))
                    {
                        if (Logger != null && Logger.TraceEnabled) Logger.Trace("Creating Connection For Use.");
                        if ((_inUseConnections.Count + _availableConnections.Count + 1) > _maxPoolSize)
                            throw new InvalidOperationException(
                                string.Format("LdapConnection pool limit of {0} exceeded.", _maxPoolSize));

                        connection = BuildConnection();
                        _inUseConnections.Add(connection);
                    }
                    else
                    {
                        if (Logger != null && Logger.TraceEnabled) Logger.Trace("Using Available Connection.");
                        _inUseConnections.Add(pair.Key);
                        _availableConnections.Remove(pair.Key);
                        connection = pair.Key;
                    }
                    if (Logger != null && Logger.TraceEnabled) Logger.Trace("In Use Connection Count: " + _inUseConnections.Count);
                    if (Logger != null && Logger.TraceEnabled) Logger.Trace("Available Connection Count: " + _availableConnections.Count);
                }
                catch (Exception ex)
                {
                    if (Logger != null) Logger.Error(ex);
                    throw;
                }
            }
            return connection;
        }

        private void TimerElapsed(object sender, ElapsedEventArgs e)
        {
            if (_disposed) throw new ObjectDisposedException(GetType().FullName);
            lock (_connectionLockObject)
            {
                try
                {
                    if (Logger != null && Logger.TraceEnabled) Logger.Trace("Available Connections Before Scavenge: " + _availableConnections.Count);
                    if (Logger != null && Logger.TraceEnabled) Logger.Trace("Scavenging Connections.");
                    int amountToScavenge = _minPoolSize == 0
                    ? _availableConnections.Count
                    : (_availableConnections.Count - _minPoolSize);

                    if (amountToScavenge <= 0) return;

                    var expiredConnections = (from pair in _availableConnections
                                              where e.SignalTime.Subtract(pair.Value).TotalMinutes > _connectionIdleTime
                                              select pair.Key).ToList();

                    foreach (var expiredConnection in expiredConnections)
                    {
                        if (amountToScavenge == 0) break;

                        if (Logger != null && Logger.TraceEnabled) Logger.Trace("Connection Scavenged.");
                        _availableConnections.Remove(expiredConnection);
                        expiredConnection.Dispose();
                        amountToScavenge--;
                    }
                    if (Logger != null && Logger.TraceEnabled) Logger.Trace("Available Connections After Scavenge: " + _availableConnections.Count);
                }
                catch (Exception ex)
                {
                    if (Logger != null) Logger.Error(ex);
                    throw;
                }
            }
        }

        public IPooledConnectionFactoryConfiguration MaxPoolSizeIs(int size)
        {
            if (size < 1) throw new ArgumentException("MaxPoolSize must be greater than zero.");
            _maxPoolSize = size;
            return this;
        }

        public IPooledConnectionFactoryConfiguration MinPoolSizeIs(int size)
        {
            if (size < 0) throw new ArgumentException("MinPoolSize cannot be negative.");
            _minPoolSize = size;
            return this;
        }

        public IPooledConnectionFactoryConfiguration ConnectionIdleTimeIs(double idleTime)
        {
            if (idleTime < 0) throw new ArgumentException("ConnectionIdleTime cannot be negative.");
            _connectionIdleTime = idleTime;
            return this;
        }

        public IPooledConnectionFactoryConfiguration ScavengeIntervalIs(double interval)
        {
            if (interval < 0) throw new ArgumentException("ScavengeInterval cannot be negative.");
            _timer.Interval = interval;
            return this;
        }
    }
}
